fix: Claude sandbox followups#340
fix: Claude sandbox followups#340gilesknap wants to merge 20 commits intoDiamondLightSource:claude-sandboxfrom
Conversation
VS Code's built-in Git extension re-injects GIT_ASKPASS and VSCODE_GIT_IPC_HANDLE per child-process spawn, overriding the remoteEnv blanking in devcontainer.json. Re-blank both on the `just claude` command line so claude cannot reach VS Code's credential bridge. README-CLAUDE.md updated to document why. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per PR DiamondLightSource#337 review: nodejs, gh, and glab are slated to move into the ubuntu-devcontainer base image once it ships on Ubuntu 26.04, where all three are available from apt at recent enough versions. Add TODO comments at each install block so the cleanup is obvious when 26.04 lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VS Code's default port auto-detection forwards the port when the user clicks the link in sphinx-autobuild output, so the explicit forwardPorts: [8000] and remote.autoForwardPorts: false override weren't pulling their weight as defaults. Per PR DiamondLightSource#337 review: this was sphinx-autobuild-specific config that crept in and shouldn't ship with every sphinx project. Removed from both the meta repo's own devcontainer.json (the add_claude=no baseline) and the templated copy, keeping the drift test happy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously postCreate.sh only existed when add_claude=yes; the
add_claude=no case used a long inline bash string in
postCreateCommand. Tidier to always have a script and gate the
Claude-specific bits with Jinja {% if add_claude %} inside it, per
PR DiamondLightSource#337 review.
The meta repo now has its own .devcontainer/postCreate.sh that
mirrors the add_claude=no rendering, and its devcontainer.json
calls it the same way. Drift test still passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
git submodule update --init runs on every postCreate, which on container rebuild moves any submodule whose HEAD differs from the parent's pinned SHA into detached HEAD — silently yanking in-progress branch work in initialized submodules. The data isn't lost (reachable via reflog and existing branches) but recovery requires submodule literacy that not every user has. Filter to only the uninitialized submodules using git submodule status output (leading '-'), preserving the first-clone build protection without touching already-checked-out submodules. Applies to all generated projects, not just the Claude-sandbox case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@coretl followup to your comments on #337 Prompt: |
Reinstate the README → README-CLAUDE.md table row removed in the
previous commit, using {{repo_url}}/blob/main/... so the link works in
both renderers:
- myst (Sphinx) treats it as an external URL and skips xref resolution,
so --fail-on-warning no longer trips on myst.xref_missing.
- GitHub renders it as a normal link to the file in the repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous absolute URL pointed at the example repo's main branch, which won't have README-CLAUDE.md until the example is regenerated with add_claude=yes — chicken-and-egg, so linkcheck 404s. Raw <a href="./README-CLAUDE.md"> works in both renderers without that dependency: - myst: passes through as inline HTML, no xref_missing warning. - sphinx linkcheck: doesn't follow raw HTML hrefs (verified locally), so no 404. - GitHub: renders as a normal clickable link to the file in the repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Always install gh and glab when add_claude is on. The base image will ship them on Ubuntu 26.04, at which point the Dockerfile install blocks can be dropped wholesale; in the meantime, having them unconditional removes two questions and the matching jinja gating across the Dockerfile, devcontainer.json, postStart.sh and justfile. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If the user opens the devcontainer before running `git init`, uv sync fails opaquely because setuptools-scm can't compute a version, and pre-commit can't install hooks into a non-existent .git/hooks. VS Code's wrapper then hides the real stderr behind a generic "postCreateCommand failed" exit-1. postCreate.sh now detects a missing .git directory up front, prints a clear explanation to stderr, and exits 1. The user is told exactly what to run on the host to recover. Failing fast beats silently bootstrapping a half-broken environment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6c08e63 to
19e9846
Compare
|
❯ how does the sandboxing look? ... ● Sandbox looks intact. Here's the picture: Defense layers (all enforced):
Live verification (just ran from this shell):
One small drift worth flagging: the glab helper is pinned to /usr/local/bin/glab while gh resolved to /usr/bin/gh. That's expected — postStart.sh:46 uses command -v glab at runtime. Just noting it because the README example at README-CLAUDE.md:38 describes rewriting away from /usr/local/bin/gh, and here the situation is mirrored for glab. Not a leak, just a path quirk. Deliberately exposed (per README-CLAUDE.md): ~/.claude bind-mount, --net=host, /workspaces parent mount, shared /cache volume. All documented as intentional trade-offs. Net: sandbox is healthy and the runtime state matches the policy. ❯ are the intentional trade offs acceptable? ● Mostly yes, with two worth thinking about harder. Quick read on each: ~/.claude bind-mount — biggest hidden surface area. With --dangerously-skip-permissions, anything Claude writes to ~/.claude/hooks/ or ~/.claude/skills/ runs on every future Claude session in every project, on the host. The README does flag this ("treat ~/.claude as part of the sandbox boundary"), but it means a single compromised prompt in this project can persist into unrelated work. Acceptable if you trust Claude not to scribble in its own config, less so as a defense-in-depth boundary. Worth periodically auditing ~/.claude/hooks/ and ~/.claude/skills/ for anything unexpected. --net=host — broad, but the justification is concrete (X11 + EPICS CA for Diamond instrument control + no devcontainer port-forwarding). Practical implication: anything bound to localhost on the host (databases, dev servers, local model endpoints, secrets daemons) is reachable from inside. If you don't run sensitive local services on the host while Claude is working, it's fine. Bridge networking would be safer but materially less useful for the use case. /workspaces parent mount — fine as long as you don't park unrelated/sensitive repos in the same parent dir. It's a habit thing, not a sandbox flaw. /cache shared volume — uv cache poisoning is the realistic risk, but bounded to template-derived containers and gated by uv's own hash verification on locked deps. Acceptable. Net: acceptable for the intended workflow. The one I'd actually do something about is ~/.claude — if you want a tighter boundary, you could split per-project via a wrapper that points --settings-dir at a project-local copy, at the cost of losing cross-project memory/skills. |
|
@coretl see above - I investigated partial RO mount of .claude but it gets messy. We don't have to bind mount .claude but it looses lots of cross project memory features and common config. |
…derived skill triggers Three sandbox guards (SSH_AUTH_SOCK, VSCODE_GIT_IPC_HANDLE, GIT_ASKPASS) match exactly what `just claude` clears, so the failure messages now name that recipe. The other guards keep their original remediations because `just claude` would not fix them. Also extend the copier-derived skill description to fire on .claude/, CLAUDE.md, and README-CLAUDE.md edits — these are template-managed but weren't listed as triggers, so the skill was being missed for sandbox edits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tings
Pin three Dev Containers settings in devcontainer.json so VS Code never
forwards host git credentials into the container in the first place,
rather than relying on env-var clearing in `just claude`:
git.terminalAuthentication: false -> stops VS Code's Git
extension from setting GIT_ASKPASS / VSCODE_GIT_IPC_HANDLE in
integrated terminals (source: vscode/extensions/git/src/askpass.ts)
dev.containers.gitCredentialHelperConfigLocation: "none"
-> stops Dev Containers
from writing a credential.helper line into /etc/gitconfig
dev.containers.copyGitConfig: false -> stops the host
~/.gitconfig (with its url.insteadOf rewrites and per-host helpers)
from being copied into the container
A compromised model can no longer reach a leaked VS Code IPC socket by
editing the hook or recipe, because the socket is never wired up.
Knock-on cleanup:
- devcontainer.json: drop the now-redundant remoteEnv blanks for
GIT_ASKPASS / VSCODE_GIT_IPC_HANDLE / VSCODE_GIT_ASKPASS_{MAIN,NODE,
EXTRA_ARGS}. The new settings prevent VS Code from setting them at
all; sandbox-check.sh catches any regression. SSH_AUTH_SOCK= stays
in remoteEnv — there is no VS Code setting alternative for it.
- justfile: drop VSCODE_GIT_IPC_HANDLE= / GIT_ASKPASS= from `just claude`
for the same reason; SSH_AUTH_SOCK= stays.
- sandbox-check.sh: env-var failures for the credential bridge now
point at "rebuild the devcontainer" since `just claude` no longer
fixes them.
- README-CLAUDE.md: rewrite the credential-injection section to lead
with the settings as the primary defence; postStart.sh and the
sandbox-check.sh hook are belt-and-braces verifications, not the
only enforcement.
Requires devcontainer rebuild for the settings to take effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Block prompt submission when a VS Code reconnect drops either gitconfig bind: /root/.gitconfig grep'd for vscode-remote-containers credential bridges, /etc/gitconfig checked via `git config --system --list` so any leaked system-scope setting (core.hooksPath, url.insteadof, http.proxy, credential.helper) trips the hook, not just credential helpers. verify-sandbox skill: replace the narrow 6c (system credential.helper only) with 6c (/etc/gitconfig masked) + 6d (system scope is empty). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Jinja's default settings keep the newline after `{% endif %}`, so the
rendered postCreate.sh ended with `bash\n\n` (a trailing blank line).
end-of-file-fixer in pre-commit flagged this, breaking lint/run and
all `test_template_*` jobs that exercise the generated project.
`{% endif -%}` strips the trailing newline; removing the blank source
line above `{% if %}` keeps the spacing tidy when add_claude is false.
Also normalises the meta repo's own .devcontainer/postCreate.sh to
match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 follow-ups against the merged
claude-sandboxbranch:just claudeinvocation.remoteEnvblanking only sets the remote server's initial environment; VS Code's Git extension re-injectsGIT_ASKPASSandVSCODE_GIT_IPC_HANDLEper child-process spawn, defeating the sandbox. Thejust clauderecipe now re-blanks both vars on the command line.README-CLAUDE.mdupdated to explain the mechanism.ubuntu-devcontaineronce it ships on Ubuntu 26.04. Comments added at each install block.forwardPorts: [8000]/remote.autoForwardPorts: false. Per PR Add claude sandbox #337 review: sphinx-autobuild-specific config that shouldn't be a default. Drift test still passes.postCreate.sh. Previously gated onadd_claude; the no-claude case used a long inline bash string. Now always rendered with Claude-specific bits wrapped in{% if add_claude %}. Per PR Add claude sandbox #337 review.git submodule statusand only initialises uninitialised entries. Applies to all generated projects.install_gh/install_glabcopier questions. gh and glab are now installed unconditionally whenadd_claudeis on, in anticipation of them moving into the base image. Removes the matching jinja gating fromDockerfile,devcontainer.json,postStart.shandjustfile.